﻿using gixoom.Dom.Score;
using System;

namespace gixoom.Dom.Block
{
    internal class BlockFacade: IBlockFacade
    {
        private IScoreFacade scoreFacade;
        private int blockIdSizeBytes;

        /// <summary>
        /// Creates BlockFacade object.
        /// </summary>
        /// <param name="scoreFacade">score facade</param>
        /// <exception cref="System.ArgumentNullException"><i>scoreFacade</i> is a null reference.</exception>
        public BlockFacade(IScoreFacade scoreFacade)
        {
            Ensure.ArgumentNotNull(scoreFacade, "scoreFacade");

            this.scoreFacade = scoreFacade;

            blockIdSizeBytes = sizeof(BlockType) + scoreFacade.ScoreSizeBytes;
        }

        /// <summary>
        /// Creates and returns block id object from the specified block type and score.
        /// </summary>
        /// <param name="blockType">block type</param>
        /// <param name="score">block's score</param>
        /// <returns>created block id</returns>
        /// <exception cref="System.ArgumentNullException"><i>score</i> is a null reference.</exception>
        public IBlockId CreateBlockId(BlockType blockType, IScore score)
        {
            Ensure.ArgumentNotNull(score, "score");

            var blockId = new BlockId(blockType, score);
            return blockId;
        }

        /// <summary>
        /// Creates block id object from the specified array of bytes where the first byte
        /// represents a block type and all the other bytes represent a score.
        /// </summary>
        /// <param name="bytes">the array to create block id object from</returns>
        /// <exception cref="System.ArgumentNullException"><i>bytes</i> is a null reference.</exception>
        /// <exception cref="System.ArgumentException"><i>bytes</i> length is invalid.</exception>
        public IBlockId CreateBlockIdFromBytes(byte[] bytes)
        {
            Ensure.ArgumentNotNull(bytes, "bytes");
            Ensure.ArrayLengthIsValid(bytes, blockIdSizeBytes, "bytes");

            var blockType = (BlockType)bytes[0];
            var score = scoreFacade.CreateScoreFromBytes(bytes, 1);
            var blockId = new BlockId(blockType, score);

            return blockId;
        }

        /// <summary>
        /// Creates block id object from the specified array of bytes starting with the specified index
        /// where the first byte represents a block type and all the other bytes represent a score.
        /// </summary>
        /// <param name="bytes">the input from which to create block id from</param>
        /// <returns>created block id</returns>
        /// <param name="bytesIndex">the index in the array at which the bytes should be read</param>
        /// <exception cref="System.ArgumentNullException"><i>bytes</i> is a null reference.</exception>
        /// <exception cref="System.ArgumentOutOfRangeException"><i>bytesIndex</i> doesn't fall into
        /// the <i>bytes</i> boundaries.</exception>
        /// <exception cref="System.ArgumentException"><i>bytesIndex</i> or <i>bytes</i> length is invalid.</exception>
        public IBlockId CreateBlockIdFromBytes(byte[] bytes, int bytesIndex)
        {
            Ensure.ArgumentNotNull(bytes, "bytes");
            Ensure.IndexWithinRange(bytesIndex, bytes, "bytesIndex");
            Ensure.EnoughItemsTillEnd(bytes, bytesIndex, blockIdSizeBytes, "bytesIndex", "bytes");

            var blockType = (BlockType)bytes[bytesIndex];
            var score = scoreFacade.CreateScoreFromBytes(bytes, bytesIndex + 1);
            var blockId = new BlockId(blockType, score);

            return blockId;
        }

        /// <summary>
        /// Serializes the specified block id to the specified array starting with the specified index.
        /// </summary>
        /// <param name="blockId">the block id which should be serialized</param>
        /// <param name="array">the array where the serialized block id should be stored</param>
        /// <param name="arrayIndex">the array index at which the serialized block id
        /// should be stored in the array</param>
        /// <returns>the next index in the array</returns>
        /// <exception cref="System.ArgumentNullException">One of the arguments is a null reference.</exception>
        /// <exception cref="System.ArgumentOutOfRangeException"><i>arrayIndex</i> doesn't fall into
        /// the <i>array</i> boundaries.</exception>
        /// <exception cref="System.ArgumentException"><i>arrayIndex</i> or <i>array</i> length is invalid.</exception>
        public int SerializeBlockId(IBlockId blockId, byte[] array, int arrayIndex)
        {
            Ensure.ArgumentNotNull(blockId, "blockId");
            Ensure.ArgumentNotNull(array, "array");
            Ensure.IndexWithinRange(arrayIndex, array, "arrayIndex");
            Ensure.EnoughItemsTillEnd(array, arrayIndex, blockIdSizeBytes, "arrayIndex", "array");

            array[arrayIndex++] = (byte)blockId.BlockType;
            var scoreBytes = blockId.Score.GetBytes();
            scoreBytes.CopyTo(array, arrayIndex);
            arrayIndex += scoreBytes.Count;

            return arrayIndex;
        }

        /// <summary>
        /// Serializes the specified block id and returns a new array of block id bytes.
        /// </summary>
        /// <param name="blockId">the block id</param>
        /// <returns>the new array of the block id bytes</returns>
        /// <exception cref="System.ArgumentNullException"><i>blockId</i> is a null reference.</exception>
        public byte[] SerializeBlockId(IBlockId blockId)
        {
            Ensure.ArgumentNotNull(blockId, "blockId");

            var blockIdBytes = new byte[blockIdSizeBytes];
            SerializeBlockId(blockId, blockIdBytes, 0);

            return blockIdBytes;
        }

        /// <summary>
        /// Creates block object from the specified block type and the specified content.
        /// </summary>
        /// <param name="blockType">block type</param>
        /// <param name="content">the content which should be used to create the block</param>
        /// <returns>created block object</returns>
        /// <exception cref="System.ArgumentNullException"><i>content</i> is a null reference.</exception>
        public IBlock CreateBlockFromContent(BlockType blockType, byte[] content)
        {
            Ensure.ArgumentNotNull(content, "content");

            var score = scoreFacade.CreateScoreFromContent(content);
            var blockId = CreateBlockId(blockType, score);
            var block = new Block(blockId, content);

            return block;
        }

        /// <summary>
        /// Creates block object from the specified range of the specified array.
        /// </summary>
        /// <param name="blockType">block type</param>
        /// <param name="content">the input to create block object for</param>
        /// <param name="offset">the offset into the byte array from which to begin using data</param>
        /// <param name="count">the number of bytes in the array to use as data</param>
        /// <returns>created block object</returns>
        /// <exception cref="System.ArgumentNullException"><i>content</i> is a null reference.</exception>
        /// <exception cref="System.ArgumentOutOfRangeException"><i>offset</i> is out of range.
        /// This parameter requires a non-negative number which is less than the <i>content</i> length.</exception>
        /// <exception cref="System.ArgumentException"><i>count</i> is an invalid value or <i>content</i> length 
        /// is invalid.</exception>
        public IBlock CreateBlockFromContent(BlockType blockType, byte[] content, int offset, int count)
        {
            Ensure.ArgumentNotNull(content, "content");
            Ensure.IndexWithinRange(offset, content, "offset");
            Ensure.OffsetAndCountAreValid(offset, count, content, "count", "content");

            var blockContent = new byte[count];
            Array.Copy(content, offset, blockContent, 0, count);
            var score = scoreFacade.CreateScoreFromContent(blockContent);
            var blockId = CreateBlockId(blockType, score);
            var block = new Block(blockId, blockContent);

            return block;
        }

        /// <summary>
        /// Creates block object from the specified array. The first element in the array
        /// is a block type. The next elements represent a score and all the other elements
        /// are a content.
        /// </summary>
        /// <param name="bytes">the input to create block object from</param>
        /// <returns>created block object or null if it cannot be created
        /// due to array inconsistency</returns>
        /// <exception cref="System.ArgumentNullException"><i>bytes</i> is a null reference.</exception>
        /// <exception cref="System.ArgumentException"><i>bytes</i> length is too short.</exception>
        public IBlock CreateBlockFromBytes(byte[] bytes)
        {
            Ensure.ArgumentNotNull(bytes, "bytes");
            Ensure.EnoughItems(bytes, blockIdSizeBytes, "bytes");

            var blockId = CreateBlockIdFromBytes(bytes, 0);
            var block = CreateBlockFromContent(blockId.BlockType, bytes, blockIdSizeBytes,
                bytes.Length - blockIdSizeBytes);

            if (!block.Id.Score.Equals(blockId.Score))
            {
                block = null;
            }

            return block;
        }

        /// <summary>
        /// Serializes the specified block to the specified array starting with the specified index.
        /// </summary>
        /// <param name="block">the block to serialize</param>
        /// <param name="array">the array where the serialized block should be stored</param>
        /// <param name="arrayIndex">the array index at which the serialized block
        /// should be stored in the array</param>
        /// <returns>the next index in the array</returns>
        /// <exception cref="System.ArgumentNullException">One of the arguments is a null reference.</exception>
        /// <exception cref="System.ArgumentOutOfRangeException"><i>arrayIndex</i> doesn't fall into
        /// the <i>array</i> boundaries.</exception>
        /// <exception cref="System.ArgumentException"><i>arrayIndex</i> or <i>array</i> length is invalid.</exception>
        public int SerializeBlock(IBlock block, byte[] array, int arrayIndex)
        {
            Ensure.ArgumentNotNull(block, "block");
            Ensure.ArgumentNotNull(array, "array");
            Ensure.IndexWithinRange(arrayIndex, array, "arrayIndex");
            Ensure.EnoughItemsTillEnd(array, arrayIndex, blockIdSizeBytes, "arrayIndex", "array");

            array[arrayIndex++] = (byte)block.Id.BlockType;
            var scoreBytes = block.Id.Score.GetBytes();
            scoreBytes.CopyTo(array, arrayIndex);
            arrayIndex += scoreBytes.Count;
            var content = block.GetContent();
            content.CopyTo(array, arrayIndex);
            arrayIndex += content.Count;

            return arrayIndex;
        }

        /// <summary>
        /// Serializes the specified block and returns a new array of block bytes.
        /// </summary>
        /// <param name="block">the block to serialize</param>
        /// <returns>the new array of block bytes</returns>
        /// <exception cref="System.ArgumentNullException"><i>block</i> is a null reference.</exception>
        public byte[] SerializeBlock(IBlock block)
        {
            Ensure.ArgumentNotNull(block, "block");
            
            byte[] blockBytes = new byte[blockIdSizeBytes + block.GetContent().Count];
            SerializeBlock(block, blockBytes, 0);

            return blockBytes;
        }

        /// <summary>
        /// Gets the number of bytes in block id (it is the fixed value).
        /// </summary>
        public int BlockIdSizeBytes
        {
            get
            {
                return blockIdSizeBytes;
            }
        }
    }
}
